﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.ObjectModel;

namespace Ns
{
    /// <summary>
    /// Represents an unit.
    /// </summary>
#if !SILVERLIGHT
    [Serializable]
#endif
    public sealed class Unit //not a struct because recursion is not supported in struct layout
        : IEquatable<Unit>
        , IComparable<Unit>
    {
        private const string PrefixUnitExponentPattern = @"(?-i)(?<Prefix>\bda|\b[y,z,a,f,p,n,µ,m,c,d,h,k,M,G,T,P,E,Z,Y]{1})(?<Unit>[\p{S}\p{P}a-zA-Z]*)(?<Exponent>[0-9]*)";
        private const string UnitExponentPattern = @"(?-i)(?<Unit>[\p{S}\p{P}a-zA-Z]*)(?<Exponent>[0-9]*)";
        private const char InnerUnitSeparator = '/';
        private static IList<string> _unitNames = new string[0];        

        /// <summary>
        /// Gets the non case-sensitive names of unit considered while parsing a <see cref="String"/> representation of an <see cref="Unit"/>.
        /// </summary>
        /// <remarks>To avoid concurrency issue, this collection is maintained with the 
        /// <see cref="AddUnitNames"/>, 
        /// <see cref="RemoveUnitNames"/> and the 
        /// <see cref="ClearUnitNames"/> methods.</remarks>
        public static ReadOnlyCollection<string> UnitNames
        {
            get
            {
                return new ReadOnlyCollection<string>(_unitNames);
            }
        }

        /// <summary>
        /// Adds names of <see cref="Unit"/> to the <see cref="UnitNames"/> collection.
        /// </summary>
        /// <param name="names">The names to add.</param>
        /// <exception cref="ArgumentNullException"><paramref name="names"/> is null.</exception>
        public static void AddUnitNames(IEnumerable<string> names)
        {
            _unitNames = _unitNames.Union(names, StringComparer.InvariantCultureIgnoreCase).ToArray();
        }

        /// <summary>
        /// Removes names of <see cref="Unit"/> from the <see cref="UnitNames"/> collection.
        /// </summary>
        /// <param name="names">The names to remove.</param>
        /// <exception cref="ArgumentNullException"><paramref name="names"/> is null.</exception>
        public static void RemoveUnitNames(IEnumerable<string> names)
        {
            _unitNames = _unitNames.Except(names, StringComparer.InvariantCultureIgnoreCase).ToArray();
        }

        /// <summary>
        /// Clears the <see cref="UnitNames"/> collection.
        /// </summary>
        public static void ClearUnitNames()
        {
            _unitNames = new string[0];
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly UnitPrefix _prefix;

        /// <summary>
        /// Gets the prefix of the unit.
        /// </summary>
        /// <value>The unit prefix.</value>
        public UnitPrefix Prefix
        {
            get { return _prefix; }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly string _name;

        /// <summary>
        /// Gets the name of the unit.
        /// </summary>
        /// <value>The unit name.</value>
        public string Name
        {
            get { return _name; }
        }

        //not an uint => need support for -exponent when multiplying
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly int _exponent;

        /// <summary>
        /// Gets the exponent of the unit.
        /// </summary>
        /// <value>The unit exponent.</value> 
        public int Exponent
        {
            get { return _exponent; }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly Unit _innerUnit;

        /// <summary>
        /// Gets the inner unit.
        /// </summary>
        /// <value>The inner unit.</value>
        public Unit InnerUnit
        {
            get { return _innerUnit; }
        }

        internal IEnumerable<Unit> InnerUnits
        {
            get
            {
                var unit = this;
                while (unit.InnerUnit != null)
                {
                    unit = unit.InnerUnit;
                    yield return new Unit(unit.Prefix, unit.Name, unit.Exponent);
                }
            }
        }

        internal IEnumerable<Unit> Units
        {
            get
            {
                yield return new Unit(Prefix, Name, Exponent);

                var unit = this;
                while (unit.InnerUnit != null)
                {
                    unit = unit.InnerUnit;
                    yield return new Unit(unit.Prefix, unit.Name, unit.Exponent);
                }
            }
        }

        private static Unit Empty
        {
            get { return new Unit(UnitPrefixes.Default, string.Empty, 1); }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Unit"/> class.
        /// </summary>
        /// <param name="value">An unit represented as a string.</param>
        /// <exception cref="ArgumentException"><paramref name="value"/> does not represent a valid unit with no duplicates.</exception>
        public Unit(string value)
        {
            var unit = default(Unit);
            if (!TryParse(value, out unit))
                throw new ArgumentException("Argument does not represent a valid unit.", "value");

            _exponent = unit.Exponent;
            _innerUnit = unit.InnerUnit;
            _name = unit.Name;
            _prefix = unit.Prefix;
        }       

        internal Unit(UnitPrefix prefix, string name, int exponent)
        {
            if (exponent < 1)
                throw new ArgumentException("Argument must be strictly positive.", "exponent");

            _prefix = prefix;
            _name = name;
            _exponent = exponent;
        }

        internal Unit(UnitPrefix prefix, string name, int exponent, Unit innerUnit)
            : this(prefix, name, exponent)
        {
            if (innerUnit != null)
            {
                _innerUnit = innerUnit;
                if (ContainsDuplicates(Units))
                    throw new ArgumentException("Unit duplicates are not authorized.");
            }
        }

        internal Unit(IList<Unit> units)
        {
            if (units == null)
                throw new ArgumentNullException("units");
            if (units.Count == 0)
                throw new ArgumentException("units");

            _prefix = units[0].Prefix;
            _name = units[0].Name;
            _exponent = units[0].Exponent;

            if (units.Count > 1)
            {
                var unit = units[units.Count - 1];
                for (int i = units.Count - 2; i >= 1; i--)
                {
                    unit = new Unit(units[i].Prefix, units[i].Name, units[i].Exponent, unit);
                }
                _innerUnit = unit;
            }
        }

        private Unit()
        {
        }

        #region Overriden Members

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return ToString(new StringBuilder(), this);
        }

        /// <summary>
        /// Recursive ToString
        /// </summary>
        private string ToString(StringBuilder builder, Unit unit)
        {
            if (builder == null)
                throw new ArgumentNullException("builder");

            //remove last delimiter
            if (unit == null)
                return builder.Remove(builder.Length - 1, 1).ToString();

            builder.AppendFormat("{0}{1}{2}/"
                , unit.Prefix.Symbol
                , unit.Name ?? string.Empty
                , unit.Exponent > 1 ? unit.Exponent.ToString() : string.Empty);

            return ToString(builder, unit.InnerUnit);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            int hash = 0x4043ed47;
            hash = (hash * -1521134295) + Prefix.GetHashCode();
            hash = (hash * -1521134295) + (Name == null ? 0 : Name.GetHashCode());
            hash = (hash * -1521134295) + Exponent.GetHashCode();
            hash = (hash * -1521134295) + (InnerUnit == null ? 0 : InnerUnit.GetHashCode());
            return hash;
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<b>true</b> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <b>false</b>.
        /// </returns>
        public override bool Equals(object obj)
        {
            return (obj is Unit)
                ? Equals((Unit)obj)
                : false;
        }

        #endregion

        #region IEquatable<Unit> Members

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        /// <remarks>Equality is case-sensitive and culture invariant.</remarks>
        public bool Equals(Unit other)
        {
            return object.ReferenceEquals(this, other)
                || Equals(other
                , UnitFields.Prefix
                | UnitFields.Name
                | UnitFields.Exponent
                | UnitFields.InnerUnit);
        }

        private bool Equals(Unit other, UnitFields fields)
        {
            return Unit.Equals(this, other, fields);
        }

        internal static bool Equals(Unit x, Unit y, UnitFields fields)
        {
            return (x == null && y == null)
                || (x != null && y != null)
                && ((UnitFields.Prefix & fields) == UnitFields.Prefix ? UnitPrefix.Equals(x.Prefix, y.Prefix) : true)
                && ((UnitFields.Name & fields) == UnitFields.Name ? string.Equals(x.Name, y.Name, StringComparison.InvariantCulture) : true)
                && ((UnitFields.Exponent & fields) == UnitFields.Exponent ? int.Equals(x.Exponent, y.Exponent) : true)
                && ((UnitFields.InnerUnit & fields) == UnitFields.InnerUnit ? Unit.Equals(x.InnerUnit, y.InnerUnit) : true);
        }

        /// <summary>
        /// Determines whether the specified <see cref="Unit"/> instances are considered equal.
        /// </summary>
        /// <param name="x">The first <see cref="Unit"/> to compare.</param>
        /// <param name="y">The second <see cref="Unit"/> to compare.</param>
        /// <returns><b>true</b> if <paramref name="x"/> is equal to <paramref name="y"/> or if both are null references; otherwise, <b>false</b>.</returns>
        public static bool Equals(Unit x, Unit y)
        {
            return Equals(x
                , y
                , UnitFields.Prefix
                | UnitFields.Name
                | UnitFields.Exponent
                | UnitFields.InnerUnit);
        }

        #endregion

        #region IComparable<Unit> Members

        /// <summary>
        /// Compares the current object with another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings:
        /// Value
        /// Meaning
        /// Less than zero
        /// This object is less than the <paramref name="other"/> parameter.
        /// Zero
        /// This object is equal to <paramref name="other"/>.
        /// Greater than zero
        /// This object is greater than <paramref name="other"/>.
        /// </returns>
        /// <exception cref="ArgumentException"><paramref name="other"/> is not comparable to this instance.</exception>
        /// <remarks>Comparison is case-sensitive and culture invariant.</remarks>
        public int CompareTo(Unit other)
        {
            if (other == null)
            {
                return 1;
            }

            if (!this.Equals(other
                , UnitFields.Name
                | UnitFields.Exponent
                | UnitFields.InnerUnit))
                throw new ArgumentException("Non comparable units", "other");

            return Prefix.CompareTo(other.Prefix);
        }

        #endregion

        #region Operators

        /// <summary>
        /// Performs an explicit conversion from <see cref="System.String"/> to <see cref="Unit"/>.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>The result of the conversion.</returns>
        public static explicit operator Unit(string value)
        {
            return new Unit(value);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="Unit"/> to <see cref="System.String"/>.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator string(Unit value)
        {
            return (value != null) ? value.ToString() : default(string);
        }       

        /// <summary>
        /// Multiplies two units.
        /// </summary>
        /// <param name="x">The <see cref="Unit"/> x.</param>
        /// <param name="y">The <see cref="Unit"/> y.</param>
        /// <returns>The result of the multiplication.</returns>        
        internal static Unit Multiply(Unit x, Unit y)
        {
            if (x == null || y == null)
                throw new ArgumentNullException("x or y");

            var units = new List<Unit>();

            //consider root unit
            if (Unit.Equals(x, y, UnitFields.Prefix | UnitFields.Name))
                units.Add(new Unit(x.Prefix, x.Name, x.Exponent + y.Exponent)); // no matching inner units because unit duplicates are not authorized

            //consider root unit & inner unit
            var yInnerUnit = (from yiu in y.InnerUnits
                              where Unit.Equals(yiu, x, UnitFields.Prefix | UnitFields.Name)
                              select yiu).FirstOrDefault(); //unit duplicates are not authorized

            if (yInnerUnit == null)
            {
                if (!units.Exists(match => string.Equals(match.Name, y.Name, StringComparison.InvariantCulture)))
                    units.Add(new Unit(x.Prefix, x.Name, x.Exponent));
            }
            else if (x.Exponent - yInnerUnit.Exponent > 0)
            {
                units.Add(new Unit(x.Prefix, x.Name, x.Exponent - yInnerUnit.Exponent));
            }

            var xInnerUnit = (from xiu in x.InnerUnits
                              where Unit.Equals(xiu, y, UnitFields.Prefix | UnitFields.Name)
                              select xiu).FirstOrDefault(); //unit duplicates are not authorized

            if (xInnerUnit == null)
            {
                if (!units.Exists(match => string.Equals(match.Name, y.Name, StringComparison.InvariantCulture)))
                    units.Add(new Unit(y.Prefix, y.Name, y.Exponent));
            }
            else if (y.Exponent - xInnerUnit.Exponent > 0)
            {
                units.Add(new Unit(y.Prefix, y.Name, y.Exponent - xInnerUnit.Exponent));
            }

            //ignore empty units if necessary
            if (units.Count > 1 && units.Exists(match => match.Name == string.Empty))
                units.RemoveAll(match => match.Name == string.Empty);

            if (units.Count > 1)
                throw new ArgumentException("Composite units are not supported.");

            //consider inner units
            var names = new List<string>();

            foreach (var xUnit in x.InnerUnits)
            {
                if (!Unit.Equals(xUnit, xInnerUnit))
                {
                    var yUnitMatch = (from yUnit in y.InnerUnits
                                      where string.Equals(yUnit.Name, xUnit.Name, StringComparison.InvariantCulture)
                                      select yUnit).FirstOrDefault();

                    if (yUnitMatch == null)
                    {
                        units.Add(xUnit);
                    }
                    else
                    {
                        //not necessary
                        //if (!names.Exists(name => string.Equals(name, xUnit.Name, StringComparison.InvariantCulture)))
                        //{
                        units.Add(new Unit(xUnit.Prefix, xUnit.Name, xUnit.Exponent + yUnitMatch.Exponent));
                        names.Add(xUnit.Name);
                        //}
                    }
                }
                else if (xInnerUnit.Exponent - y.Exponent > 0)
                    units.Add(new Unit(xInnerUnit.Prefix, xInnerUnit.Name, xInnerUnit.Exponent - y.Exponent));
            }

            foreach (var yUnit in y.InnerUnits)
            {
                if (!Unit.Equals(yUnit, yInnerUnit))
                {
                    var xUnitMatch = (from xUnit in x.InnerUnits
                                      where string.Equals(xUnit.Name, yUnit.Name, StringComparison.InvariantCulture)
                                      select xUnit).FirstOrDefault();

                    if (xUnitMatch == null)
                    {
                        units.Add(yUnit);
                    }
                    else
                    {
                        if (!names.Exists(name => string.Equals(name, yUnit.Name, StringComparison.InvariantCulture)))
                        {
                            units.Add(new Unit(yUnit.Prefix, yUnit.Name, yUnit.Exponent + xUnitMatch.Exponent));
                            //not necessary
                            //names.Add(yUnit.Name);
                        }
                    }
                }
                else if (yInnerUnit.Exponent - x.Exponent > 0)
                {
                    units.Add(new Unit(yInnerUnit.Prefix, yInnerUnit.Name, yInnerUnit.Exponent - x.Exponent));
                }
            }

            //sort inner units alphabetically if necessary
            if (units.Count > 2)
            {
                var innerUnits = new Unit[units.Count - 1];
                units.CopyTo(1, innerUnits, 0, units.Count - 1);
                units.RemoveRange(1, units.Count - 1);
                units.AddRange(from iu in innerUnits orderby iu.Name select iu);
            }

            return units.Count != 0
                ? new Unit(units)
                : null;
        }

        /// <summary>
        /// Divides two units.
        /// </summary>
        /// <param name="x">The <see cref="Unit"/> x.</param>
        /// <param name="y">The <see cref="Unit"/> y.</param>
        /// <returns>The result of the division.</returns>        
        /// <exception cref="ArgumentException"><paramref name="x"/> is not comparable to <paramref name="y"/>.</exception>
        internal static Unit Divide(Unit x, Unit y)
        {
            if (x == null || y == null)
                throw new ArgumentNullException("x or y");

            var yReversed = Reverse(y);
            return Multiply(x, yReversed);
        }

        private static Unit Reverse(Unit unit)
        {
            if (unit == null)
                throw new ArgumentNullException("unit");

            var units = unit.Units.ToList();

            if (units.Count == 1)
            {
                units.Insert(0, new Unit(UnitPrefixes.Default, string.Empty, 1));
                return new Unit(units);
            }
            else if (units.Count > 1)
            {
                var u = units[0];
                units[0] = units[1];
                units[1] = u;
                return new Unit(units);
            }

            throw new InvalidOperationException();
        }

        #endregion

        /// <summary>
        /// Converts a string to an <see cref="Unit"/>.
        /// A return value indicates whether the operation succeeded.
        /// </summary>
        /// <param name="s">A string containing a unit to convert.</param>
        /// <param name="result">When this method returns, contains the <see cref="Unit"/> equivalent to the <paramref name="s"/> parameter, 
        /// if the conversion succeeded, or stays uninitialized if the conversion failed.
        /// The conversion fails if the <paramref name="s"/> parameter is a <b>null</b> null reference, or is not of the correct format. 
        /// This parameter is passed uninitialized.</param>
        /// <returns><b>true</b> if <paramref name="s"/> was converted successfully; otherwise, <b>false</b>.</returns>
        public static bool TryParse(string s, out Unit result)
        {
            result = null;
            if (s == null)
                return false;

            var col = s.Split(InnerUnitSeparator);
            var units = new List<Unit>(col.Length);
            
            foreach (var item in col)
            {
                //consider defined unitNames
                var match = Regex.Match(item.Trim(), UnitExponentPattern);
                if (match.Success
                    && match.Groups["Unit"].Length != 0
                    && UnitNames.Contains(match.Groups["Unit"].Value, StringComparer.InvariantCultureIgnoreCase))
                {
                    units.Add(new Unit(UnitPrefixes.Default
                        , match.Groups["Unit"].Value
                        , match.Groups["Exponent"].Value.Length != 0
                        ? int.Parse(match.Groups["Exponent"].Value)
                        : 1
                        , null));

                    continue;
                }

                //consider undefined unitNames
                match = Regex.Match(item.Trim(), PrefixUnitExponentPattern);
                if (!match.Success)
                {
                    units.Add(new Unit(UnitPrefixes.Default, item, 1));
                    continue;
                }

                //consider prefix
                var prefix = (match.Groups["Prefix"].Length != 0 && match.Groups["Unit"].Length == 0)
                    ? default(string)
                    : match.Groups["Prefix"].Length != 0
                    ? match.Groups["Prefix"].Value
                    : default(string);

                //consider name
                var name = (match.Groups["Prefix"].Length != 0 && match.Groups["Unit"].Length == 0)
                    ? match.Groups["Prefix"].Value
                    : match.Groups["Unit"].Length != 0
                    ? match.Groups["Unit"].Value
                    : default(string);

                //consider exponent
                var exponent = match.Groups["Exponent"].Value.Length != 0
                    ? int.Parse(match.Groups["Exponent"].Value)
                    : 1;

                units.Add(new Unit((UnitPrefix)prefix, name, exponent, null));
            }

            if (ContainsDuplicates(units))
                return false;

            result = new Unit(units);
            return true;
        }

        private static bool ContainsDuplicates(IEnumerable<Unit> units)
        {
            var groups = from u in units
                         group u by u.Name into g
                         select g;

            var r = (from g in groups
                     where g.Count() > 1
                     select g).FirstOrDefault();

            return (r != null);
        }
    }
}